package cmich.cps.svn.utility;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import org.apache.commons.io.FileUtils;

import cmich.cps.svn.utility.gui.Prompts;

/**
 * The Class Repository.
 *
 * @author Jonathan Kissinger "<kissi1j@cmich.edu>"
 */
public class Repository {

	/** The Constant LOGGER. */
	private final static Logger LOGGER = Logger.getLogger(Repository.class.getName());

	/** The prompts. */
	private Prompts prompts = Prompts.getInstance();

	/** The system. */
	private UtilitySystem system = UtilitySystem.getInstance();

	/** The local directory. */
	private File localDir;

	/** The assignments. */
	private List<Assignment> assignments = new ArrayList<Assignment>();

	/** The active assignment. */
	private Assignment activeAssignment;

	/** The student list. */
	private List<String> studentList = new ArrayList<String>();
	
	/** The settings. */
	private Settings settings = Settings.getInstance();

	/** The trunk directory. */
	private File trunkDir;

	/**
	 * Instantiates a new repository.
	 *
	 * @param localDir the local directory
	 */
	public Repository(File localDir) {
		super();
		this.localDir = localDir;
		trunkDir = new File(this.localDir + File.separator + "trunk");
		if(!trunkDir.exists()) {
			trunkDir = this.localDir;
		}
	}

	/**
	 * Initialize variables.
	 */
	public void initialize() {
		setStudents();
		setAssignments();
	}

	/**
	 * Sets the students.
	 */
	private void setStudents() {
		File studentFile = new File(trunkDir + File.separator + "students.txt");
		LOGGER.fine("Looking for student file at " + studentFile.getPath());
		try {
			FileReader fReader = new FileReader(studentFile);
			BufferedReader bReader = new BufferedReader(fReader);
			String line = bReader.readLine();
			while(line!=null) {
				if(!this.studentList.contains(line)) {
					this.studentList.add(line);
				}
				line = bReader.readLine();
			}
			bReader.close();
		} catch (FileNotFoundException e) {
			String msg = "The students.txt file is missing it should contain all student global id's separated by lines and it should be located in the trunk directory";
			LOGGER.warning(msg);
			prompts.nonFatalError(msg);
		} catch (IOException e) {
			String msg = "Error reading from students.txt file in repository " + this.toString() + " " + e.toString();
			LOGGER.warning(msg);
			prompts.nonFatalError(msg);
		}
	}

	/**
	 * Sets {@link #assignments} to contain all the assignments in this repository.
	 */
	private void setAssignments() {
		this.assignments.clear();
		this.activeAssignment = null;
		File[] dirs = trunkDir.listFiles();
		for(File dir : dirs) {
			if(dir.isDirectory() && !dir.getName().equals(".svn")) {
				Assignment assign = new Assignment(dir);
				this.assignments.add(assign);
				LOGGER.finest("Now managing assignment " + assign);
			}
		}
		if(this.assignments.size()==0) {
			addNewAssignment();
		}
	}

	/**
	 * Returns an unmodifiable list of assignments belonging to this repository.
	 * 
	 * @return An unmodifiable list of assignments belonging to this repository.
	 */
	public List<Assignment> getAssignments() {
		return Collections.unmodifiableList(this.assignments);
	}

	/**
	 * Returns an unmodifiable list of assignments belonging to this repository.
	 * 
	 * @return An unmodifiable list of assignments belonging to this repository.
	 */
	public List<String> getStudents() {
		return Collections.unmodifiableList(this.studentList);
	}

	/**
	 * Adds a new assignment to this repository.
	 */
	public void addNewAssignment() {
		String assignmentName = prompts.getInput("What would you like to call the new assignment?");
		if(assignmentName==null || assignmentName.isEmpty()) {
			return;
		}
		if(assignmentName.contains(" ")) {
			String msg = "The assignment name cannot contain spaces.";
			LOGGER.fine(msg);
			prompts.nonFatalError(msg);
			return;
		}
		File dir = new File(trunkDir + File.separator + assignmentName);
		Assignment assignment = new Assignment(dir);
		Date date = new Date();
		String dueDate = prompts.getDueDate(settings.sdf.format(date));
		if(dueDate==null) {
			LOGGER.config("User chose not to provide a due date for the assignment.");
			return;
		}
		if(assignment.create(this.studentList)) {
			LOGGER.config("Added new assignment: " + assignment.getPath());
			this.assignments.add(assignment);
			setActiveAssignment(assignment);
			add();
			commit("Initial commit of new assignment");
			setProperties(true, false, dueDate);
		}
		else {
			try {
				FileUtils.deleteDirectory(dir);
			} catch (IOException e) {
				String msg = "Failed to delete directory: " + dir;
				LOGGER.warning(msg);
				prompts.nonFatalError(msg);
			}
		}
	}

	/**
	 * Returns the active assignment if there is one, if not sets one, if none is available prompts the
	 * user to add one and sets it as active.
	 * 
	 * @return The active assignment
	 */
	public Assignment getActiveAssignment() {
		if(this.activeAssignment==null) {
			if(this.assignments.size()==0) {
				addNewAssignment();
			}
			int primary = 0;
			for(int i=0;i<this.assignments.size();i++) {
				if(!this.assignments.get(i).isPastDue()) {
					primary = i;
					break;
				}
			}
			setActiveAssignment(this.assignments.get(primary));
		}
		return this.activeAssignment;
	}

	/**
	 * Adds the file to active assignment.
	 */
	public void addFileToActiveAssignment() {
		File file = prompts.getFileToAdd(null);
		if(file!=null) {
			this.activeAssignment.addFile(this.studentList, file);
			String msg = "Added new file " + file.getName() + " to student directories in assignment " + this.activeAssignment;
			add();
			commit(msg);
			LOGGER.info(msg);
		}
		else {
			LOGGER.fine("User didn't choose a file to add, cancelling adding of file to assignment.");
		}
	}

	/**
	 * Removes the file from {@link #activeAssignment}
	 */
	public void removeFileFromActiveAssignment() {
		String studentZero = this.studentList.get(0);
		File startDir = new File(this.activeAssignment.getPath() + File.separator + studentZero);
		File file = prompts.getFileToRemove(startDir);
		if(file!=null) {
			String deleteMe = file.getAbsolutePath().replace(this.activeAssignment.getPath() + File.separator + studentZero, "");
			String confirmMsg = "Are you sure you want to delete " + deleteMe + " from all student directories in " + this.activeAssignment + "?";
			if(prompts.confirm(confirmMsg)) {
				for(String student : this.studentList) {
					String studentDir = this.activeAssignment.getPath() + File.separator + student;
					String myFile =  studentDir + deleteMe;
					system.delete(myFile);
				}
				String msg = "Deleting " + file.getName() + " from student directories in assignment " + this.activeAssignment;
				commit(msg);
				LOGGER.info(msg);
			}
			else {
				LOGGER.fine("User chose to cancel file removal.");
			}
		}
		else {
			LOGGER.fine("User didn't choose a file to add, cancelling adding of file to assignment.");
		}
	}

	/**
	 * Adds a unit test.
	 */
	public void addUnitTest() {
		File file = prompts.getFileToAdd(null);
		if(file!=null) {
			if(this.activeAssignment.addUnitTest(file)) {
				String msg = "Adding new unit test " + file.getName() + " to " + this.activeAssignment;
				add();
				commit(msg);
				LOGGER.info(msg);
			}
		}
		else {
			LOGGER.fine("User didn't choose a file to add, cancelling adding of file to assignment.");
		}
	}

	/**
	 * Sets the active assignment.
	 *
	 * @param assign the new active assignment
	 */
	public void setActiveAssignment(Assignment assign) {
		LOGGER.fine("Active assignment changing to " + assign);
		this.activeAssignment = assign;
	}

	/**
	 * Calls add on {@link #svnCommand(String, String)}
	 *
	 * @param file the file to add
	 * @return true, if successful
	 */
	public boolean add() {
		String result = "";
		if(this.activeAssignment!=null) {
			String directory = this.activeAssignment.getPath();
			result = system.svnCommand("add", "--force " + settings.enclose(directory));
		}
		else {
			for(File file : this.trunkDir.listFiles()) {
				result = result + system.svnCommand("add", "--force " + settings.enclose(file.getPath()));
			}
		}
		
		if(result.startsWith("A ") || result.startsWith("svn: Skipping argument") || result.equals("")) {
			return true;
		}
		LOGGER.info("svn add failed, with result: " + result);
		return false;
	}

	/**
	 * Commit.
	 *
	 * @param message the commit message
	 * @return true, if successful
	 */
	public boolean commit(String message) {
		message = "SVNUtility: " + message;
		String result = system.svnCommand("commit", "-m \"" + message + "\" " + getLocalPath());
		LOGGER.config("svn commit completed, with result: " + result);
		if(result.contains("out of date") && !message.startsWith("Second attempt: ")) {
			LOGGER.fine("Commit failed, repository out of date attempting to update.");
			update();
			commit("Second attempt: " + message);
		}
		else if(result.contains("Commit failed")) {
			String error = result;
			if(result.contains("(details follow)")) {
				error = result.split("\\(details follow\\):")[1];
			}
			String msg = "Commit failed: " + error;
			LOGGER.warning(msg);
			prompts.fatalError(msg + "\n\nUse an external program to resolve these issues.");
			return false;
		}
		return true;
	}

	/**
	 * Update.
	 */
	public void update() {
		String result = system.svnCommand("update", getLocalPath());
		LOGGER.info("svn update completed, with result: " + result);
	}

	/**
	 * Status.
	 *
	 * @return true, if clear
	 */
	public boolean status() {
		String result = system.svnCommand("status", getLocalPath());
		if(result.equals("")) {
			LOGGER.finer("Status came back valid for " + this);
			return true;
		}
		else if(result.contains("is not a working copy")) {
			LOGGER.finest(this + " is not a repository.");
			return false;
		}
		else if(result.startsWith("M ") || result.startsWith("D ") || result.startsWith("A ")) {
			LOGGER.config("Repository " + this + " modified externally.");
			if(prompts.confirm(this + " has uncommited modifications would you like to commit them and recheck the status?")) {
				LOGGER.fine("User chose to commit external modifications.");
				commit("Repository modified externally, attempting to commit changes.");
				return status();
			}
		}
		else if(result.startsWith("? ")) {
			result = result.replace("\\n", "<br>");
			result = result.replace("? ", "");
			String msg = "Unversioned files exist in repository.";
			LOGGER.info(msg);
			prompts.nonFatalError(msg + "<br>" + result);
			if(prompts.confirm("Would you like to add these files to the repository?")) {
				add();
				commit("Added unversioned files at user request.");
				return status();
			}
			else {
				prompts.message("This utility cannot run with unversioned files in the repository, please store them elsewhere.");
				return false;
			}
		}
		else {
			String message = "Local repository " + this + " has issues that need to be corrected with an external subversion editor.  You will not be able to use the repository with this utility until the issues are corrected.";
			LOGGER.warning(message + "\nStatus results: " + result);
			prompts.nonFatalError(message);
			return false;
		}
		return false;
	}

	public void setProperties(Boolean aLC, Boolean rCL, String stringDueDate) {
		system.setProperty("allowLateCommits", aLC.toString(), activeAssignment.getEnclosedPath());
		system.setProperty("requireCommitLog", rCL.toString(), activeAssignment.getEnclosedPath());
		try {
			Date dueDate = settings.sdf.parse(stringDueDate);
			system.setProperty("dueDate", settings.sdf.format(dueDate), activeAssignment.getEnclosedPath());
		} catch (ParseException e) {
			String msg = "Invalid due date format. " + stringDueDate + " Due date will not be changed.";
			LOGGER.config(msg);
			prompts.nonFatalError(msg);
		}
		add();
		commit("Properties changed.");
	}

	/**
	 * Gets the local path.
	 *
	 * @return the local path
	 */
	public String getLocalPath() {
		return settings.enclose(localDir.getPath());
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return this.localDir.getName();
	}
}
